// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/app_list/pagination_model.h"

#include <algorithm>

#include "ui/app_list/pagination_model_observer.h"
#include "ui/gfx/animation/slide_animation.h"

namespace app_list {

PaginationModel::PaginationModel()
    : total_pages_(-1)
    , selected_page_(-1)
    , transition_(-1, 0)
    , pending_selected_page_(-1)
    , transition_duration_ms_(0)
    , overscroll_transition_duration_ms_(0)
    , last_overscroll_target_page_(0)
{
}

PaginationModel::~PaginationModel()
{
}

void PaginationModel::SetTotalPages(int total_pages)
{
    if (total_pages == total_pages_)
        return;

    total_pages_ = total_pages;
    if (selected_page_ < 0)
        SelectPage(0, false /* animate */);
    if (selected_page_ >= total_pages_)
        SelectPage(std::max(total_pages_ - 1, 0), false /* animate */);
    FOR_EACH_OBSERVER(PaginationModelObserver, observers_, TotalPagesChanged());
}

void PaginationModel::SelectPage(int page, bool animate)
{
    if (animate) {
        // -1 and |total_pages_| are valid target page for animation.
        DCHECK(page >= -1 && page <= total_pages_);

        if (!transition_animation_) {
            if (page == selected_page_)
                return;

            // Suppress over scroll animation if the same one happens too fast.
            if (!is_valid_page(page)) {
                const base::TimeTicks now = base::TimeTicks::Now();

                if (page == last_overscroll_target_page_) {
                    const int kMinOverScrollTimeGapInMs = 500;
                    const base::TimeDelta time_elapsed = now - last_overscroll_animation_start_time_;
                    if (time_elapsed.InMilliseconds() < kMinOverScrollTimeGapInMs)
                        return;
                }

                last_overscroll_target_page_ = page;
                last_overscroll_animation_start_time_ = now;
            }

            // Creates an animation if there is not one.
            StartTransitionAnimation(Transition(page, 0));
            return;
        } else {
            const bool showing = transition_animation_->IsShowing();
            const int from_page = showing ? selected_page_ : transition_.target_page;
            const int to_page = showing ? transition_.target_page : selected_page_;

            if (from_page == page) {
                if (showing)
                    transition_animation_->Hide();
                else
                    transition_animation_->Show();
                pending_selected_page_ = -1;
            } else if (to_page != page) {
                pending_selected_page_ = page;
            } else {
                pending_selected_page_ = -1;
            }
        }
    } else {
        DCHECK(total_pages_ == 0 || (page >= 0 && page < total_pages_));

        if (page == selected_page_)
            return;

        ResetTransitionAnimation();

        int old_selected = selected_page_;
        selected_page_ = page;
        NotifySelectedPageChanged(old_selected, selected_page_);
    }
}

void PaginationModel::SelectPageRelative(int delta, bool animate)
{
    SelectPage(CalculateTargetPage(delta), animate);
}

void PaginationModel::FinishAnimation()
{
    SelectPage(SelectedTargetPage(), false);
}

void PaginationModel::SetTransition(const Transition& transition)
{
    // -1 and |total_pages_| is a valid target page, which means user is at
    // the end and there is no target page for this scroll.
    DCHECK(transition.target_page >= -1 && transition.target_page <= total_pages_);
    DCHECK(transition.progress >= 0 && transition.progress <= 1);

    if (transition_.Equals(transition))
        return;

    transition_ = transition;
    NotifyTransitionChanged();
}

void PaginationModel::SetTransitionDurations(int duration_ms,
    int overscroll_duration_ms)
{
    transition_duration_ms_ = duration_ms;
    overscroll_transition_duration_ms_ = overscroll_duration_ms;
}

void PaginationModel::StartScroll()
{
    // Cancels current transition animation (if any).
    transition_animation_.reset();
}

void PaginationModel::UpdateScroll(double delta)
{
    // Translates scroll delta to desired page change direction.
    int page_change_dir = delta > 0 ? -1 : 1;

    // Initializes a transition if there is none.
    if (!has_transition())
        transition_.target_page = CalculateTargetPage(page_change_dir);

    // Updates transition progress.
    int transition_dir = transition_.target_page > selected_page_ ? 1 : -1;
    double progress = transition_.progress + fabs(delta) * page_change_dir * transition_dir;

    if (progress < 0) {
        if (transition_.progress) {
            transition_.progress = 0;
            NotifyTransitionChanged();
        }
        clear_transition();
    } else if (progress > 1) {
        if (is_valid_page(transition_.target_page)) {
            SelectPage(transition_.target_page, false);
            clear_transition();
        }
    } else {
        transition_.progress = progress;
        NotifyTransitionChanged();
    }
}

void PaginationModel::EndScroll(bool cancel)
{
    if (!has_transition())
        return;

    StartTransitionAnimation(transition_);

    if (cancel)
        transition_animation_->Hide();
}

bool PaginationModel::IsRevertingCurrentTransition() const
{
    // Use !IsShowing() so that we return true at the end of hide animation.
    return transition_animation_ && !transition_animation_->IsShowing();
}

void PaginationModel::AddObserver(PaginationModelObserver* observer)
{
    observers_.AddObserver(observer);
}

void PaginationModel::RemoveObserver(PaginationModelObserver* observer)
{
    observers_.RemoveObserver(observer);
}

int PaginationModel::SelectedTargetPage() const
{
    // If no animation, or animation is in reverse, just the selected page.
    if (!transition_animation_ || !transition_animation_->IsShowing())
        return selected_page_;

    // If, at the end of the current animation, we will animate to another page,
    // return that eventual page.
    if (pending_selected_page_ >= 0)
        return pending_selected_page_;

    // Just the target of the current animation.
    return transition_.target_page;
}

void PaginationModel::NotifySelectedPageChanged(int old_selected,
    int new_selected)
{
    FOR_EACH_OBSERVER(PaginationModelObserver,
        observers_,
        SelectedPageChanged(old_selected, new_selected));
}

void PaginationModel::NotifyTransitionStarted()
{
    FOR_EACH_OBSERVER(PaginationModelObserver, observers_, TransitionStarted());
}

void PaginationModel::NotifyTransitionChanged()
{
    FOR_EACH_OBSERVER(PaginationModelObserver, observers_, TransitionChanged());
}

int PaginationModel::CalculateTargetPage(int delta) const
{
    DCHECK_GT(total_pages_, 0);
    const int target_page = SelectedTargetPage() + delta;

    int start_page = 0;
    int end_page = total_pages_ - 1;

    // Use invalid page when |selected_page_| is at ends.
    if (target_page < start_page && selected_page_ == start_page)
        start_page = -1;
    else if (target_page > end_page && selected_page_ == end_page)
        end_page = total_pages_;

    return std::max(start_page, std::min(end_page, target_page));
}

void PaginationModel::StartTransitionAnimation(const Transition& transition)
{
    DCHECK(selected_page_ != transition.target_page);

    NotifyTransitionStarted();
    SetTransition(transition);

    transition_animation_.reset(new gfx::SlideAnimation(this));
    transition_animation_->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
    transition_animation_->Reset(transition_.progress);

    const int duration = is_valid_page(transition_.target_page) ? transition_duration_ms_ : overscroll_transition_duration_ms_;
    if (duration)
        transition_animation_->SetSlideDuration(duration);

    transition_animation_->Show();
}

void PaginationModel::ResetTransitionAnimation()
{
    transition_animation_.reset();
    transition_.target_page = -1;
    transition_.progress = 0;
    pending_selected_page_ = -1;
}

void PaginationModel::AnimationProgressed(const gfx::Animation* animation)
{
    transition_.progress = transition_animation_->GetCurrentValue();
    NotifyTransitionChanged();
}

void PaginationModel::AnimationEnded(const gfx::Animation* animation)
{
    // Save |pending_selected_page_| because SelectPage resets it.
    int next_target = pending_selected_page_;

    if (transition_animation_->GetCurrentValue() == 1) {
        // Showing animation ends.
        if (!is_valid_page(transition_.target_page)) {
            // If target page is not in valid range, reverse the animation.
            transition_animation_->Hide();
            return;
        }

        // Otherwise, change page and finish the transition.
        DCHECK(selected_page_ != transition_.target_page);
        SelectPage(transition_.target_page, false /* animate */);
    } else if (transition_animation_->GetCurrentValue() == 0) {
        // Hiding animation ends. No page change should happen.
        ResetTransitionAnimation();
    }

    if (next_target >= 0)
        SelectPage(next_target, true);
}

} // namespace app_list
